//-----------------------------------------------------------------------
// <copyright file="CompareFilesInFragments.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
// </copyright>
// <author>Shaun Wilde (ICS)</author>
// <email>v-swilde</email>
// <date>2006-08-17</date>
// <summary>TODO</summary>
//-----------------------------------------------------------------------

namespace Microsoft.Sdc.Tasks.Wix
{
  #region Using directives
  using System.Collections;
  using System.Xml;
  using Microsoft.Build.Framework;
  #endregion

  /// <summary>
  /// Compares 2 Wix fragment files and publishes file differences between them.
  /// Items of interest are that are detected are:
  ///   files that are in one but not the other
  /// </summary>
  /// <remarks>
  /// <para>
  /// Dynamically generated fragments are considered bad news when it comes to release 
  /// installers as the component GUIDs keep changing and as such issues arise when 
  /// making patches. This task can be used to compare a wix fragment that has been 
  /// generated now with one generated early on and then baselined when the project is 
  /// stable and release date is near.
  /// </para>
  /// <para>
  /// <b>About Wix:</b> Wix was originally a Microsoft tool that has been released into the open source
  /// community under the Common Public License. The binaries and source code can be downloaded from
  /// <a href="http://sourceforge.net/projects/wix">SourceForge</a>.
  /// </para>
  /// </remarks>
  public class CompareFilesInFragments : TaskBase
  {
    #region Member Variables
    private string wixFragmentFile1;
    private string wixFragmentFile2;
    private string[] missingFiles1;
    private string[] missingFiles2;
    private bool ignoreMissingFiles = false;
    private ArrayList arMissingFiles1 = new ArrayList();
    private ArrayList arMissingFiles2 = new ArrayList();
    #endregion

    #region Public Properties
    /// <summary>
    /// A wix fragment file
    /// </summary>
    [Required]
    public string FragmentFile1
    {
      get { return wixFragmentFile1; }
      set { wixFragmentFile1 = value; }
    }

    /// <summary>
    /// A wix fragment file
    /// </summary>
    [Required]
    public string FragmentFile2
    {
      get { return wixFragmentFile2; }
      set { wixFragmentFile2 = value; }
    }

    /// <summary>
    /// A list of files found in FragmentFile1 but not in FragmentFile2
    /// </summary>
    [Output]
    public string[] MissingFiles1
    {
      get
      {
        return this.missingFiles1;
      }
      set
      {
        this.missingFiles1 = value;
      }
    }

    /// <summary>
    /// A list of files found in FragmentFile2 but not in FragmentFile1
    /// </summary>
    [Output]
    public string[] MissingFiles2
    {
      get
      {
        return this.missingFiles2;
      }
      set
      {
        this.missingFiles2 = value;
      }
    }

    /// <summary>
    /// Do not treat missing files as errors - instead record them in the outputs MissingFiles1 and MissingFiles2
    /// </summary>
    public bool IgnoreMissingFiles
    {
      get
      {
        return this.ignoreMissingFiles;
      }
      set
      {
        this.ignoreMissingFiles = value;
      }
    }

    #endregion

    #region Protected Members
    /// <summary>
    /// compare the fragments
    /// </summary>
    protected override void InternalExecute()
    {
      // first determine if files are passable as wix fragments
      // #1 are they xml?
      // #2 do thay have a wix/fragment node
      XmlDocument doc1 = new XmlDocument();
      doc1.Load(wixFragmentFile1);

      XmlDocument doc2 = new XmlDocument();
      doc2.Load(wixFragmentFile2);

      XmlNamespaceManager nsmgr1 = new XmlNamespaceManager(doc1.NameTable);
      nsmgr1.AddNamespace("ns","http://schemas.microsoft.com/wix/2003/01/wi");
      XmlNode nodeFragment1 = doc1.SelectSingleNode("/ns:Wix/ns:Fragment", nsmgr1);

      if (nodeFragment1 == null)
      {
        throw new TaskException("CompareFragment.NotFragment", wixFragmentFile1);
      }

      XmlNamespaceManager nsmgr2 = new XmlNamespaceManager(doc2.NameTable);
      nsmgr2.AddNamespace("ns","http://schemas.microsoft.com/wix/2003/01/wi");
      XmlNode nodeFragment2 = doc2.SelectSingleNode("/ns:Wix/ns:Fragment", nsmgr2);

      if (nodeFragment2 == null)
      {
        throw new TaskException("CompareFragment.NotFragment", wixFragmentFile2);
      }

      // get all file nodes of file 1 and 2
      XmlNodeList listFiles1 = doc1.SelectNodes("//ns:File", nsmgr1);
      XmlNodeList listFiles2 = doc2.SelectNodes("//ns:File", nsmgr2);

      bool missingFiles = false;
      bool structureDifferent = false;
      // report on files that are in 1 but not in 2
      CompareFragments(listFiles1, listFiles2, false, ref missingFiles, ref structureDifferent);
      // report on files that are in 2 but not in 1
      CompareFragments(listFiles2, listFiles1, true, ref missingFiles, ref structureDifferent);

      if (missingFiles)
      {
        if (arMissingFiles1.Count > 0)
        {
          this.missingFiles1 = new string[arMissingFiles1.Count];
          arMissingFiles1.CopyTo(this.missingFiles1);
        }

        if (arMissingFiles2.Count > 0)
        {
          this.missingFiles2 = new string[arMissingFiles2.Count];
          arMissingFiles2.CopyTo(this.missingFiles2);
        }
      }

      if ((!ignoreMissingFiles && missingFiles) || structureDifferent)
      {
        throw new TaskException("CompareFragment.DoNoMatch");
      }

    }

    /// <summary>
    /// Compare the file lists in each fragment
    /// </summary>
    /// <param name="listFiles1">list of files found in first fragment</param>
    /// <param name="listFiles2">list of files found in second fragment</param>
    /// <param name="reverse">true when the list are switched</param>
    /// <param name="missingFiles">set to true if files missing</param>
    /// <param name="structureDifferent">set to true if structure different</param>
    private void CompareFragments(XmlNodeList listFiles1, XmlNodeList listFiles2, bool reverse, ref bool missingFiles, ref bool structureDifferent)
    {
      string frag1 = (reverse)? wixFragmentFile2 : wixFragmentFile1;
      string frag2 = (reverse)? wixFragmentFile1 : wixFragmentFile2;
      ArrayList arList = (reverse) ? arMissingFiles2 : arMissingFiles1;
      foreach (XmlNode attr1 in listFiles1)
      {
        string shortfileName1 = attr1.Attributes["Name"].Value;
        string longfileName1 = (attr1.Attributes.GetNamedItem("LongName") == null) ? null : attr1.Attributes["LongName"].Value;
        string reportingName = longfileName1 ?? shortfileName1;
        bool found = false;
        bool suspect = false;
        string source = attr1.Attributes["src"].Value;
        foreach (XmlNode attr2 in listFiles2)
        {
          string shortfileName2 = attr2.Attributes["Name"].Value;
          string longfileName2 = (attr2.Attributes.GetNamedItem("LongName") == null) ? null : attr2.Attributes["LongName"].Value;
          if ((longfileName1 != null) && (longfileName2 != null) && (longfileName1 == longfileName2))
          {
            found = true;
          }
          else if ((longfileName1 == null) && (longfileName2 == null) && (shortfileName1 == shortfileName2))
          {
            found = true;
          }

          if (found)
          {
            // do the parents contain the same number of files - component rules could be broken
            XmlNode parent1 = attr1.ParentNode;
            XmlNode parent2 = attr2.ParentNode;
            if (parent1.ChildNodes.Count != parent2.ChildNodes.Count)
            {
              structureDifferent = true;
              Log.LogError("{0} in fragment {1} was found in fragment {2} but their parent nodes contain a differing number of children",
                reportingName, frag1, frag2);
            }

            // check the parent of parent nodes is the file in the same relative location in each fragment - 
            // this is a simple test and not as exhaustive as it possibly could be
            XmlNode grandParent1 = attr1.ParentNode.ParentNode;
            XmlNode grandParent2 = attr2.ParentNode.ParentNode;
            suspect = DirectoryNodeTest(suspect, grandParent1, grandParent2);

            if (!suspect)
            {
              break;
            }
          }
        }

        if (suspect)
        {
          structureDifferent = true;
          Log.LogError("{0} in fragment {1} was found in fragment {2} but in a different location",
            reportingName, frag1, frag2);
        }

        if (!found)
        {
          missingFiles = true;
          if (!ignoreMissingFiles)
          {
            Log.LogError("{0} in fragment {1} was not found in fragment {2}", reportingName,
              frag1, frag2);
          }
          arList.Add(source);

        }
      }
    }

    /// <summary>
    /// Test the directory parent structure above the matching files
    /// </summary>
    /// <param name="suspect">is node considered suspect but awaiting proof</param>
    /// <param name="directoryNode1">a directory parent node</param>
    /// <param name="directoryNode2">a directory parent node</param>
    /// <returns>true if structure is supect</returns>
    private static bool DirectoryNodeTest(bool suspect, XmlNode directoryNode1, XmlNode directoryNode2)
    {
      if (directoryNode1.LocalName == directoryNode2.LocalName)
      {
        if (directoryNode1.LocalName == "Directory")
        {
          string folderName1 = (directoryNode1.Attributes.GetNamedItem("LongName") == null) ? directoryNode1.Attributes["Name"].Value : directoryNode1.Attributes["LongName"].Value;
          string folderName2 = (directoryNode2.Attributes.GetNamedItem("LongName") == null) ? directoryNode2.Attributes["Name"].Value : directoryNode2.Attributes["LongName"].Value;
          if (folderName1 != folderName2)
          {
            suspect = true;
          }
          else
          {
            suspect = false;
          }

          if (!suspect)
          {
            suspect = DirectoryNodeTest(suspect, directoryNode1.ParentNode, directoryNode2.ParentNode);
          }
        }
        else if (directoryNode1.LocalName != "DirectoryRef")
        {
          throw new TaskException("CompareFragment.DoNoMatch");
        }
      }
      return suspect;
    }
    #endregion
  }
}
